#ifndef VISUALTREATMENT_H
#define VISUALTREATMENT_H

/********************************************************************

created:	2024/02/03
updated:    2025/12/19
filename: 	visualtreatment.h
file base:	visualtreatment
file ext:	h
author:		HellRaven Mods

purpose:	Visualtreatment-Shader for NFS ProStreet

*********************************************************************/

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CONSTANTS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
shared float4 cvBlurParams : cvBlurParams;
shared float4 cvTextureOffset : cvTextureOffset;
shared float4 cvVisualEffectFadeColour : cvVisualEffectFadeColour;

static const int MAX_SAMPLES = 16;	// Maximum texture grabs

// Contains	sampling offsets used by the techniques 
shared float4 cavSampleOffsetWeights[MAX_SAMPLES]	: REG_cavSampleOffsetWeights;
shared float4 cavSampleOffsets[MAX_SAMPLES]         : REG_cavSampleOffsets;

// Tone	mapping	variables
shared float cfBloomScale : cfBloomScale;

shared float4 Coeffs0 : CURVE_COEFFS_0;
shared float4 Coeffs1 : CURVE_COEFFS_1;

shared float4 cvDepthOfFieldParams : cvDepthOfFieldParams;
shared float cfDrawDepthOfField : cfDrawDepthOfField;

// Depth of Field variables
shared bool cbDrawDepthOfField : cbDrawDepthOfField;
shared bool cbDepthOfFieldEnabled : cbDepthOfFieldEnabled;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Samplers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
texture custom_tex1;
sampler CUSTOM_SAMPLER = sampler_state
{
    Texture = <custom_tex1>;
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;
    AddressU = Clamp;
    AddressV = Clamp;
};

texture custom_tex2;
sampler CUSTOM_SAMPLER1 = sampler_state
{
    Texture = <custom_tex2>;
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = Linear;
    AddressU = Clamp;
    AddressV = Clamp;
};

texture lut2d_tex;
sampler LUT2D_SAMPLER = sampler_state
{
    Texture = <lut2d_tex>;
    MinFilter = Linear;
    MagFilter = Linear;
    MipFilter = None;   // no Mips, to prevent cross-talk
    AddressU = Clamp;
    AddressV = Clamp;
};

DECLARE_TEXTURE(DIFFUSE_TEXTURE)
sampler	DIFFUSE_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(DIFFUSE_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(MOTIONBLUR_TEXTURE)
sampler	MOTIONBLUR_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(MOTIONBLUR_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(DEPTHBUFFER_TEXTURE)
sampler	DEPTHBUFFER_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(DEPTHBUFFER_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = LINEAR;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(MISCMAP6_TEXTURE)				// needed for the PC
sampler	MISCMAP6_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(MISCMAP6_TEXTURE)			   // needed for the PC
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = LINEAR;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

DECLARE_TEXTURE(BLOOM_TEXTURE)
sampler BLOOM_SAMPLER = sampler_state
{
    ASSIGN_TEXTURE(BLOOM_TEXTURE)
    AddressU = CLAMP;
    AddressV = CLAMP;
    MIPFILTER = NONE;
    MINFILTER = LINEAR;
    MAGFILTER = LINEAR;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Depth Sprite
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

struct DepthSpriteOut
{
    float4 colour : COLOR0;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Structs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

struct VS_INPUT_SCREEN
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
};

struct VtoP
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
};

VtoP vertex_shader_passthru(const VS_INPUT_SCREEN IN)
{
    VtoP OUT;
    OUT.position = IN.position;
    OUT.tex0 = IN.tex0;
    OUT.tex1 = IN.tex1;

    return OUT;
}

float4 PS_PassThru(const VtoP IN) : COLOR
{
    float4 OUT;

    float4 diffuse = tex2D(DIFFUSE_SAMPLER, IN.tex0.xy);	// * IN;
    OUT.xyz = diffuse.xyz;
    OUT.w = diffuse.w;

    return OUT;
}

struct PS_INPUT
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Texelsize + Time (drawn from CPU)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float2 texelSizeL0;
float2 texelSizeL1;
float2 texelSizeL2;
float2 texelSizeL3;
float2 texelSizeL4;
float Time;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Motionblur Quality Defines
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static const int   SAMPLES = 16;
static const float kWeights[SAMPLES] = { 2.6,1.5,1.4,1.3,1.2,1.1,1.0,0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.05 };
static const float jitterAmount = 0.0070;
static const float ditheramount = 0.008;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Generate Noise Texture
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float2 Hash22(float2 p)
{
    // Dave Hoskins style hash, SM3-friendly (no trig)
    float3 p3 = frac(float3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return frac((p3.xx + p3.yz) * p3.zy);
}

float generateNoise(float2 uv)
{
    const int OCTAVES = 2;
    float total = 0.0;
    float amplitude = 1.0;
    float maxAmplitude = 0.0;
    float frequency = 2.0;

    [loop]
        for (int i = 0; i < OCTAVES; i++)
        {
            float seed = dot(uv * frequency, float2(12.9898, 78.233));
            float noise = frac(sin(seed) * 43758.5453);
            noise = noise * 2.0 - 1.0;

            total += noise * amplitude;
            maxAmplitude += amplitude;

            amplitude *= 0.5;
            frequency *= 2.0;
        }

    return total / maxAmplitude;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Generate HQ Jitter
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Produces a stable unit direction (cos,sin) from hashed noise.
float2 generateHQJitter(int index, float2 baseTex)
{
    // Mix uv + index so each tap gets a different direction
    float2 h = Hash22(baseTex * 4096.0 + float2(index * 17.0, index * 59.0));

    // Map to [-1..1]
    float2 v = h * 2.0 - 1.0;

    // Avoid degenerate vectors and normalize (acts like (cos,sin))
    float len2 = max(dot(v, v), 1e-4);
    v *= rsqrt(len2);

    // Keep your original radial distribution behaviour
    float radius = sqrt(max(float(index), 0.0001) / float(SAMPLES));

    return v * radius * jitterAmount;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Motion Blur
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Definitions
static const float baseRampCoeff = 0.75; // Base coefficient, dynamically modulated

// Motion Blur Vertex Shader
struct VS_INPUT_MOTIONBLUR
{
    float4 position : POSITION;
    float4 tex0 : TEXCOORD0;
    float4 tex1 : TEXCOORD1;
    float4 tex2 : TEXCOORD2;
    float4 tex3 : TEXCOORD3;
    float4 tex4 : TEXCOORD4;
    float4 tex5 : TEXCOORD5;
    float4 tex6 : TEXCOORD6;
    float4 tex7 : TEXCOORD7;
};

struct VtoP_MOTIONBLUR
{
    float4 position : POSITION;
    float2 tex[8] : TEXCOORD0;
};

// Vertex Shader
VtoP_MOTIONBLUR VS_MotionBlur(const VS_INPUT_MOTIONBLUR IN)
{
    VtoP_MOTIONBLUR OUT;
    OUT.position = IN.position;

    OUT.tex[0] = IN.tex0.xy;
    OUT.tex[1] = IN.tex1.xy;
    OUT.tex[2] = IN.tex2.xy;
    OUT.tex[3] = IN.tex3.xy;
    OUT.tex[4] = IN.tex4.xy;
    OUT.tex[5] = IN.tex5.xy;
    OUT.tex[6] = IN.tex6.xy;
    OUT.tex[7] = IN.tex7.xy;

    return OUT;
}

// Motion Blur Pixel Shader
float4 PS_MotionBlur(const VtoP_MOTIONBLUR IN) : COLOR
{
    float4 result = 0;

    // Always run full quality loop (16 samples)
    int loopCount = 16;
    float weightSum = 0.0;

    // Pre-calculate weight sum for HQ
    [unroll]
    for (int wi = 0; wi < 16; wi++) {
        weightSum += kWeights[wi];
    }

    for (int i = 0; i < 16; i++)
    {
        float2 baseTex = IN.tex[i % 8];

        // Always use HQ Jitter logic
        float2 hqJitter = baseTex + generateHQJitter(i, IN.tex[0]);

        float2 jitteredTex = saturate(hqJitter);
        float4 sample = tex2D(MOTIONBLUR_SAMPLER, jitteredTex);
        result += sample * kWeights[i];
    }

    result /= max(weightSum, 1e-5);

    float4 firstSample = tex2D(MOTIONBLUR_SAMPLER, IN.tex[0]);
    float dynamicRampCoeff = saturate(baseRampCoeff + (firstSample.w * 0.25));
    result = lerp(firstSample, result, dynamicRampCoeff);

    // Dither remains useful for banding prevention
    float dither = generateNoise(IN.tex[0] * 123.456);
    result.rgb += dither * ditheramount;

    return saturate(result);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Apply Motion Blur
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

VtoP VS_CompositeBlur(const VS_INPUT_SCREEN IN)
{
    VtoP OUT;

    OUT.position = IN.position;
    OUT.tex0 = IN.tex0;
    OUT.tex1 = IN.tex1;

    return OUT;
}

DepthSpriteOut PS_CompositeBlur(const VtoP IN)
{
    DepthSpriteOut OUT;

    float4 blur = tex2D(MOTIONBLUR_SAMPLER, IN.tex0.xy);
    float4 screen = tex2D(DIFFUSE_SAMPLER, IN.tex0.xy);

    float2 scaledMaskUV = (IN.tex1.xy - 0.5) * ui_MotionBlurMask + 0.5;

    // Always use HQ Vignette Texture (CUSTOM_SAMPLER2)
    float4 vignette = tex2D(CUSTOM_SAMPLER1, scaledMaskUV);

    // Always use HQ Logic
    // HQ Logic: (-v.x) * p.x
    // HQ Logic: (-v.y) + p.y
    float term1 = -vignette.x;
    float term2 = -vignette.y;

    float motionBlurMask = term1 * cvBlurParams.x;
    float radialBlurMask = term2 + cvBlurParams.y;

    float finalMask = saturate(motionBlurMask + radialBlurMask);

    float blurIntensity = blur.w;
    float finalBlend = saturate(blurIntensity * finalMask);

    OUT.colour.rgb = lerp(screen.rgb, blur.rgb, finalBlend);
    OUT.colour.a = screen.a;

    return OUT;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Fake HDR function - ported from ReShade but optimized by me
// Not actual HDR - It just tries to mimic an HDR look
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyHDR(const VtoP IN, in float4 result)
{
    // Use a simple fake HDR bloom by sampling along eight directions around the current pixel.
    // Radius is now in PIXELS, scaled by texelSizeL0 -> resolution independent.
    float HDRPower = ui_FakeHDRPower;

    // Radius in pixels (tweak freely; same "look" across resolutions now)
    float radius1Px = 2.0;
    float radius2Px = 1.9;

    float2 stepUV = texelSizeL0;

    float3 color = result.rgb;

    float3 bloom_sum1 = float3(0.0, 0.0, 0.0);
    float3 bloom_sum2 = float3(0.0, 0.0, 0.0);

    // Predefined unit circle directions to avoid cos/sin in the loop
    static const float2 directions[8] = {
        float2(1.0,  0.0),
        float2(0.70710678,  0.70710678),
        float2(0.0,  1.0),
        float2(-0.70710678,  0.70710678),
        float2(-1.0,  0.0),
        float2(-0.70710678, -0.70710678),
        float2(0.0, -1.0),
        float2(0.70710678, -0.70710678)
    };

    [loop]
        for (int i = 0; i < 4; ++i)
        {
            float2 dir = directions[i];

            float2 uv1 = IN.tex0.xy + dir * (radius1Px * stepUV);
            float2 uv2 = IN.tex0.xy + dir * (radius2Px * stepUV);

            // Safety clamp
            uv1 = saturate(uv1);
            uv2 = saturate(uv2);

            bloom_sum1 += tex2D(DIFFUSE_SAMPLER, uv1).rgb;
            bloom_sum2 += tex2D(DIFFUSE_SAMPLER, uv2).rgb;
        }

    bloom_sum1 *= (1.0 / 4.0);
    bloom_sum2 *= (1.0 / 4.0);

    float dist = radius2Px - radius1Px;
    float3 HDR = (color + (bloom_sum2 - bloom_sum1)) * dist;
    float3 blend = HDR + color;

    // Protect against zero and NaN
    float3 safeBlend = max(abs(blend), 0.0001);
    float3 hdrColor = blend * pow(safeBlend, HDRPower - 1.0) + HDR;

    return float4(saturate(hdrColor), result.w);
}


////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Vanilla Depth Of Field
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 DoDepthOfField(const VtoP IN, in float4 result, float depth)
{
    float zDist = (1 / (1 - depth));
    float focalDist = cvDepthOfFieldParams.x;
    float depthOfField = cvDepthOfFieldParams.y;
    float falloff = cvDepthOfFieldParams.z;
    float maxBlur = cvDepthOfFieldParams.w;
    float blur = saturate((abs(zDist - focalDist) - depthOfField) * falloff / zDist);
    float mipLevel = blur * maxBlur;
    float3 blurredTex = tex2Dbias(MISCMAP6_SAMPLER, float4(IN.tex0.xy, 0, mipLevel));
    result = float4(lerp(result.xyz, blurredTex, saturate(mipLevel)), result.w);

    //result.xyz = saturate((abs(zDist-50)-30)*falloff/zDist);
    if (cbDrawDepthOfField)
    {
        result.x += (1 - blur) * 0.3;						// draw the depth of field falloff
        result.xyz += (blur == 0.0f) ? 0.5 : 0.0f;	// draw the crital focal point
    }

    return result;
}

/////////////////////////////////////////////////////////////////////////////////////////
// Hybrid Bokeh Depth of Field (Corrected)
// 1. Calculates Blur Factor based on depth.
// 2. Samples the Game's Downsampled Map (MISCMAP6) for base blur (solves dark edges).
// 3. Adds BLOOM_SAMPLER on top to simulate light bokeh shapes.
// 4. Blends mostly correctly without "foggy" look on dark objects.
/////////////////////////////////////////////////////////////////////////////////////////

// Removed #if USE_BOKEH_DOF == 1 wrapper to allow dynamic usage
float4 applyHybridBokehDoF(const VtoP IN, float4 sceneColor, float depth)
{
    // Hardcoded defaults
    // float ui_DOFfocusDist = 12.0;
    // float ui_DOFfocusRange = 50.0;
    // float ui_DOFblurMax = 0.55;
    // float ui_DOFshapeInt = 1.25;

    // --- 1. Calculate Distance & Factor ---
    float zDist = 1.0 / (1.0 - depth);
    float distFromFocus = abs(zDist - ui_DOFfocusDist);
    float blurFactor = smoothstep(0.0, ui_DOFfocusRange, distFromFocus);
    blurFactor = min(blurFactor, ui_DOFblurMax);

    // Early exit - performance branch (sometimes useful)
    if (blurFactor <= 0.01) return sceneColor;

    float dynamicBias = blurFactor * 4.0;

    // --- 2. Sample Base Blur (The "Matsch") ---
    // We use MISCMAP6 (Vanilla DoF Texture) but bias it to use lower mips immediately.
    // This creates the physical blur foundation.
    float4 baseBlur = tex2Dbias(MISCMAP6_SAMPLER, float4(IN.tex0.xy, 0, dynamicBias)); // 3.0 bias for blurriness 

    // --- 3. Sample Bokeh Shapes (The "Shine") ---
    // We use the Bloom texture to add the "aperture shape" highlights.
    float2 caOffset = float2(0.0025, 0.0) * blurFactor; // Skaliert mit Blur

    float3 bokehHighlights;
    bokehHighlights.r = tex2D(BLOOM_SAMPLER, IN.tex0.xy + caOffset).r;
    bokehHighlights.g = tex2D(BLOOM_SAMPLER, IN.tex0.xy).g;
    bokehHighlights.b = tex2D(BLOOM_SAMPLER, IN.tex0.xy - caOffset).b;

    // --- 4. Combine ---
    // The blurred image is the Base Blur + Boosted Highlights
    float3 totalBlur = baseBlur.rgb + (bokehHighlights * ui_DOFshapeInt * blurFactor);

    // --- 5. Final Mix ---
    // Blend sharp scene with our constructed bokeh image
    float4 result;
    result.rgb = lerp(sceneColor.rgb, totalBlur, blurFactor);
    result.a = sceneColor.a;

    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Film Halation
// Simulates light scattering in film emulsion layers.
// Uses a blurred/biased sample of the Bloom texture and tints it warm/red.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 applyHalation(float3 sceneColor, float2 uv)
{
    // 1. Sample Bloom with Bias (Blur)
    // We access a less blurred version of the bloom (MipMap Level via Bias).
    // This makes the red glow wider than the actual light.
    float3 bloom = tex2Dbias(BLOOM_SAMPLER, float4(uv, 0, ui_HalationRadius)).rgb;

    // 2. Luminance Check
    // We only want this effect with really bright light sources.
    float lum = dot(bloom, LUMINANCE_VECTOR);

    // 3. Threshold & Curve
    // Everything below the threshold is truncated (max(0, ...)).
    // pow() ensures a smooth ramp-up so it doesn't "switch on" abruptly.
    float factor = max(0.0, lum - ui_HalationThreshold);
    factor = factor * factor; // Square curve for an organic look

    // 4. Tint & Mix
    // We color the bloom red / orange and add it to the image.
    float3 halationGlow = bloom * ui_HalationTint.rgb * factor * ui_HalationStrength;

    return sceneColor + halationGlow;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Desaturation Shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyDesaturation(float4 result, float amount)
{
    float luminance = dot(result.rgb, LUMINANCE_VECTOR); // Calculate Luminance
    float3 desaturatedColor = lerp(result.rgb, float3(luminance, luminance, luminance), amount);
    return float4(desaturatedColor, result.a); // Keep Alphachannel
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Curve Shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Legacy Tone Mapping using Coeffs0-Coeffs3
float3 LegacyCurveTonemap(float3 color)
{
    float luminance = dot(color, LUMINANCE_VECTOR);

    // Ensure coefficients are valid, otherwise use fallback values
    float4 curve0 = Coeffs0;
    float4 curve1 = Coeffs1;

    if (all(curve0 == 0.0) || all(curve1 == 0.0)) {
        curve0 = float4(1.0, 1.0, 1.0, 0.0);  // Fallback to neutral curve
        curve1 = float4(0.5, 0.5, 0.5, 0.0);
    }

    // Compute curve normally
    float4 curve = curve1 * luminance + curve0;

    float3 adjustedColor = color * curve.xyz;

    // Debugging: Ensure colors never fully disappear
    // adjustedColor = max(adjustedColor, 0.15); // Minimum brightness level
    adjustedColor = lerp(color, adjustedColor, 0.6);

    return saturate(adjustedColor);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Color Grading & Split Toning + Contrast Adjustment
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 applySplitToning(float3 color)
{
    float luminance = dot(color, LUMINANCE_VECTOR);
    float shadowAmount = saturate(1.0 - smoothstep(0.0, 0.5, luminance));
    float highlightAmount = saturate(smoothstep(0.5, 1.0, luminance));
    float3 shadowTint = float3(0.2, 0.3, 0.7);
    float3 highlightTint = float3(1.0, 0.8, 0.6);
    color = lerp(color, shadowTint, shadowAmount * 0.15);
    color = lerp(color, highlightTint, highlightAmount * 0.30);
    return saturate(color);
}

float3 AdjustLumaChroma(float3 color, float lumaGain, float chromaGain)
{
    float luma = dot(color, LUMINANCE_VECTOR);
    float3 gray = float3(luma, luma, luma);
    float3 chroma = color - gray;

    color = gray * lumaGain + chroma * chromaGain;

    return saturate(color);
}

float3 AdjustContrast(float3 color, float contrast)
{
    return saturate((color - 0.5) * contrast + 0.5);
}

float3 processColor(float3 color) {
    // 1. Define matrix series
    float4 rowR;
    float4 rowG;
    float4 rowB;
    float luma;
    float chroma;

    // 2. Read input(preset or custom)
    // We use an explicit check here. If the preset value is greater than 0.5, we overwrite it.
    // Otherwise, we use the UI values ​​by default.

    // Set default to custom UI values ​​(Safest way)
    rowR = ui_RedChannel;
    rowG = ui_GreenChannel;
    rowB = ui_BlueChannel;
    luma = ui_Luma;
    chroma = ui_Chroma;

    // If preset is selected, override (function call)
    if (ui_PresetSelector > 0.5) {
        GetDynamicPresetValues((int)ui_PresetSelector, rowR, rowG, rowB, luma, chroma);
    }

    // 3. Build matrix (explicit)
    // float3x3 constructor takes 3 float3 vectors as series.
    float3x3 colorMatrix = float3x3(rowR.rgb, rowG.rgb, rowB.rgb);

    // 4. Apply
    // mul(vector, matrix) is the standard for color transformations
    color = saturate(mul(color, colorMatrix));

    // 5. Remaining effects
    // Split Tone
    if (ui_SplitTone > 0.5) color = applySplitToning(color);

    // Luma/Chroma & Contrast
    color = AdjustLumaChroma(color, luma, chroma);
    color = AdjustContrast(color, ui_Contrast);

    return saturate(color);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 2D LUT application
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 apply2DLUT(float3 color)
{
    // --- LUT layout ---
    // 8x8 tiles, each tile is 32x32 pixels => full LUT is 256x256
    const float LUT_SIZE = 32.0;
    const float LUT_TILES = 8.0;
    const float LUT_SLICES = 64.0;

    // Clamp input so we never address outside [0..1]
    color = saturate(color);

    // Convert blue to slice index [0..63]
    float blue = color.b * (LUT_SLICES - 1.0);
    float slice0 = floor(blue);
    float sliceFrac = blue - slice0;

    // Clamp slices to valid range (critical fix)
    slice0 = clamp(slice0, 0.0, LUT_SLICES - 1.0);
    float slice1 = min(slice0 + 1.0, LUT_SLICES - 1.0);

    // Map slices to tile coordinates (8x8)
    float2 tile0;
    tile0.x = fmod(slice0, LUT_TILES);
    tile0.y = floor(slice0 / LUT_TILES);

    float2 tile1;
    tile1.x = fmod(slice1, LUT_TILES);
    tile1.y = floor(slice1 / LUT_TILES);

    // Pixel center offset inside tile (prevents edge bleeding)
    float2 pixel = color.rg * (LUT_SIZE - 1.0);
    float2 texel = (pixel + 0.5) / (LUT_SIZE * LUT_TILES);

    // UV inside the full atlas
    float2 uv0 = (tile0 / LUT_TILES) + texel;
    float2 uv1 = (tile1 / LUT_TILES) + texel;

    // Safety clamp (keeps sampling inside atlas even with precision quirks)
    uv0 = saturate(uv0);
    uv1 = saturate(uv1);

    float3 c0 = tex2Dlod(LUT2D_SAMPLER, float4(uv0, 0.0, 0.0)).rgb;
    float3 c1 = tex2Dlod(LUT2D_SAMPLER, float4(uv1, 0.0, 0.0)).rgb;

    return lerp(c0, c1, sliceFrac);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Color Temperatur
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 adjustTemperature(float3 color, float temperature)
{
    // temperature in [-1, +1] ≈ small shifts
    float rScale = 1.0 + 0.10 * temperature;
    float bScale = 1.0 - 0.10 * temperature;

    float3 wb = float3(rScale, 1.0, bScale); // keep energy in check
    return saturate(color * wb);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Sharpening Shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float LumaFromRGB(float3 c)
{
    // Prefer vector if present; otherwise use Rec.709-ish fallback.
#ifdef LUMINANCE_VECTOR
    return dot(c, LUMINANCE_VECTOR);
#else
    return dot(c, float3(0.2126, 0.7152, 0.0722));
#endif
}

float4 applySharpen(const VtoP IN, float4 result)
{
    float2 uv = IN.tex0.xy;

    // 1) Cross samples
    float3 c = result.rgb;
    float3 n = tex2D(DIFFUSE_SAMPLER, uv + float2(0.0, -texelSizeL0.y)).rgb;
    float3 s = tex2D(DIFFUSE_SAMPLER, uv + float2(0.0, texelSizeL0.y)).rgb;
    float3 w = tex2D(DIFFUSE_SAMPLER, uv + float2(-texelSizeL0.x, 0.0)).rgb;
    float3 e = tex2D(DIFFUSE_SAMPLER, uv + float2(texelSizeL0.x, 0.0)).rgb;

    // 2) CAS uses a contrast-adaptive gate derived from local min/max.
    // We'll compute it from luma to preserve hue.
    float lc = LumaFromRGB(c);
    float ln = LumaFromRGB(n);
    float ls = LumaFromRGB(s);
    float lw = LumaFromRGB(w);
    float le = LumaFromRGB(e);

    float mn = min(lc, min(ln, min(ls, min(lw, le))));
    float mx = max(lc, max(ln, max(ls, max(lw, le))));

    // 3) FidelityFX CAS-style "amplification" term.
    // amp grows when there is room before clipping and when contrast supports sharpening.
    float invMx = rcp(mx + 1e-5);
    float amp = saturate(min(mn, 2.0 - mx) * invMx);

    // 4) Strength mapping to CAS peak (negative weight).
    // ui_SharpenAmount expected in [0..1]. Higher => stronger sharpening.
    float a = saturate(ui_SharpenAmount);

    // CAS peak mapping: stronger peak as 'a' increases (matches common CAS tuning).
    // This keeps the denominator safely positive for the typical range.
    float peak = -rcp(lerp(8.0, 5.0, a));

    // Contrast-adaptive weight (negative), with sqrt shaping (CAS signature).
    float wLuma = peak * sqrt(amp);

    // 5) Apply convolution with negative neighbor weights.
    float denom = 1.0 + 4.0 * wLuma;

    // Safety: should already be > 0 with the peak mapping, but keep it bulletproof.
    denom = max(denom, 1e-3);

    float3 outColor = (c + (n + s + w + e) * wLuma) / denom;

    // CAS typically doesn't do an extra lerp here; the peak handles "amount".
    // Clamp to [0..1] at the end for post-processing safety in DX9.
    return float4(saturate(outColor), result.a);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Adaptive Whitebalance
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float3 adaptiveWhiteBalance(float3 color)
{
    float3 sceneColor = color;

    // Temperature proxy: Balance red against blue dominance
    float tempShift = dot(sceneColor.rgb, float3(1.0, 0.0, -1.0));

    // Apply automatic correction towards neutral white
    float3 correction = float3(-tempShift, 0.0, tempShift) * 0.1;
    color += correction;

    // Optional green/magenta shift correction
    float greenShift = sceneColor.g - ((sceneColor.r + sceneColor.b) * 0.5);
    color.rg += float2(0.01, -0.01) * greenShift;

    return saturate(color);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Bloom Helpers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float BloomKnee(float x, float threshold, float softKnee)
{
    // UE-style soft-knee curve. Returns extracted energy (>=0).
    float knee = threshold * softKnee;
    float d = x - threshold;

    // Hard part
    float hard = max(d, 0.0);

    // Soft part
    float soft = max(d + knee, 0.0);
    soft = (knee > 1e-5) ? (soft * soft) / (4.0 * knee + 1e-5) : 0.0;

    return max(hard, soft);
}

float3 ApplyBloomThresholdSoftKnee(float3 bloomColor)
{
    float lum = dot(bloomColor, LUMINANCE_VECTOR);

    float extracted = BloomKnee(lum, ui_BloomThreshold, ui_BloomSoftKnee);

    // Convert extracted energy into a scale factor for RGB (preserves hue)
    float scale = extracted / max(lum, 1e-5);

    bloomColor *= scale;

    return bloomColor;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// LensDirt shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 applyLensDirt(const VtoP IN, float4 baseColor) : COLOR
{
    // 1. Fetch Bloom & Dirt Texture (Simulate Chromatic Aberration for Quality)
    float4 dirtTex;

    // Chroma Shift Implementation: Sample dirt 3 times if chroma > 0
    if (ui_LensDirtChroma > 0.001) {
        // Used texelSizeL0 for resolution independent scaling.
        // Multiplier 200.0 turns the 0.0-0.05 slider into a roughly 0-10 pixel shift.
        // At 1080p, 0.05 * 200.0 = 10 pixels. 
        float2 offset = float2(ui_LensDirtChroma, 0.0) * texelSizeL0 * 200.0;

        float r = tex2D(CUSTOM_SAMPLER, IN.tex0.xy + offset).r;
        float g = tex2D(CUSTOM_SAMPLER, IN.tex0.xy).g;
        float b = tex2D(CUSTOM_SAMPLER, IN.tex0.xy - offset).b;
        dirtTex = float4(r, g, b, 1.0);
    }
    else {
     dirtTex = tex2D(CUSTOM_SAMPLER, IN.tex0.xy);
    }

    float4 bloomTex = tex2D(BLOOM_SAMPLER, IN.tex0.xy);

    // 2. Calculate Intensity from Bloom Luminance
    float bloomLum = dot(bloomTex.rgb, LUMINANCE_VECTOR);

    // 3. Contrast Boost for Dirt Texture
    float3 sharpDirt = pow(dirtTex.rgb, 2.2);

    // 4. Threshold Logic
    float dirtIntensity = max(0.0, bloomLum - ui_LensDirtThreshold);
    dirtIntensity = pow(dirtIntensity, 1.25) * ui_LensDirtIntensity * 4.0;

    // 5. Edge Masking (Restored Feature)
    // Calculates distance from center to fade out dirt at the edges (or inverse if power is flipped)
    float2 center = float2(0.5, 0.5);
    float dist = length(IN.tex0.xy - center) * 2.0;
    float edgeMask = saturate(pow(dist, ui_LensDirtEdgePower));
    // Usually "dirtStrength *= edgeMask" means dirt is stronger at edges (Vignette style).
    dirtIntensity *= edgeMask;

    // 6. Combine
    float3 dirtLayer = bloomTex.rgb * sharpDirt * dirtIntensity;

    // 7. Blending (User Choice)
    float3 additive = baseColor.rgb + dirtLayer;
    float3 screen = 1.0 - (1.0 - baseColor.rgb) * (1.0 - dirtLayer);

    // LERP blend mode: 0.0 = Additive, 1.0 = Screen
    return float4(lerp(additive, screen, ui_LensDirtBlendMode), baseColor.a);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Filmgrain shader
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float getGrainNoise(float2 uv)
{
    return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}

float4 applyFilmGrain(const VtoP IN, float4 result) : COLOR
{
    // Sample a noise texture to simulate film grain
    float2 noiseUV = IN.tex0.xy * 5.0f;

    float t = frac(Time * 0.223f);
    float noise = getGrainNoise(noiseUV + t);

    // Apply the noise to the screen texture (additive)
    // result.rgb += (noise - 0.5) * FILM_GRAIN_STRENGTH;

    // Reduce grain in dark areas for a more natural look
    float luminance = dot(result.rgb, LUMINANCE_VECTOR);
    result.rgb += (noise - 0.5) * ui_FilmGrainStrength * (0.5 + luminance);

    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Vignette Constants
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static const float2 VIGNETTE_CENTER = float2(0.5, 0.5);

// Screen Vignette function
float4 applyVignette(VtoP IN, float4 result) : COLOR
{
    float2 d = (IN.tex0.xy - VIGNETTE_CENTER) / max(ui_VignetteRadius, 1e-3);
    float r2 = dot(d,d);

    // smooth falloff – no hard ring, only darken edges
    float v = exp(-ui_VignetteCurve * r2);
    float f = lerp(1.0, v, ui_VignetteAmount);

    result.rgb *= f;
    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Shader Pass
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 ShaderPass(const VtoP IN, in float4 result)
{
    // Variable Declarations (At Start)
    float3 halationBloom;
    float lum;
    float threshold;
    float factor;
    float4 bloomEffect;
    float3 bloomColor;

    // --- STAGE 1: SCENE REFERRED (Linear Space / Physics) ---

    // Whitebalance
    if (ui_UseWhitebalance > 0.5) result.rgb = adaptiveWhiteBalance(result.rgb);

    // Halation Logic
    if (ui_HalationStrength > 0.001) result.rgb = applyHalation(result.rgb, IN.tex0.xy);

    // --- STAGE 2: TONEMAPPING (The Camera Sensor) ---

    // Tonemap
    result.rgb = ApplyTonemap(result.rgb, (int)ui_TonemapSelector);

    // --- STAGE 3: DISPLAY REFERRED (Styling / Grading) ---

    result.rgb = adjustTemperature(result.rgb, ui_ColorTemp);

    // LUT
    if (ui_UseLUT > 0.5) result.rgb = apply2DLUT(result.rgb);

    // Desaturation
    result = applyDesaturation(result, ui_DesaturationAmount);

    // Legacy Curves
    if (ui_useLegacyCurve > 0.5) result.rgb = LegacyCurveTonemap(result.rgb);

    // Color Processing
    result.rgb = processColor(result.rgb);

    // --- STAGE 4: OPTICAL & LENS DEFECTS (Post-Process) ---

    // Bloom
    if (ui_BloomIntensity > 0.01) 
    {
        bloomEffect = tex2D(BLOOM_SAMPLER, IN.tex0.xy);
        bloomColor = ApplyBloomThresholdSoftKnee(bloomEffect.rgb * ui_BloomIntensity);
        result.rgb = lerp(1.0f - (1.0f - result.rgb) * (1.0f - bloomColor), result.rgb + bloomColor, BLOOM_BLEND_MODE);
    }

    // Use LERP instead of IF branches for overlays.
    // This saves registers by computing the effect and multiplying by 0 if OFF.

    // LensDirt
    float4 dirtRes = applyLensDirt(IN, result);
    result = lerp(result, dirtRes, step(0.01, ui_LensDirtIntensity));

    // FilmGrain
    float4 grainRes = applyFilmGrain(IN, result);
    result = lerp(result, grainRes, step(0.001, ui_FilmGrainStrength));

    // Vignette
    float4 vignetteRes = applyVignette(IN, result);
    result = lerp(result, vignetteRes, step(0.01, ui_VignetteAmount));

    return result;

}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// you shouldnt touch these
// Visualtreatment Function
////////////////////////////////////////////////////////////////////////////////////////////////////////////////

float4 PS_VisualTreatment(const VtoP IN, uniform bool doDepthOfField, uniform bool doColourFade) : COLOR
{
    // Predefenitions
    float depth = tex2D(DEPTHBUFFER_SAMPLER, IN.tex0.xy).r;
    float4 result = tex2D(DIFFUSE_SAMPLER, IN.tex0.xy);
    float luminance = dot(result.xyz, LUMINANCE_VECTOR);

    // Fix X4550: Sharpening LERP instead of IF
    float4 sharpRes = applySharpen(IN, result);
    result = lerp(result, sharpRes, step(0.01, ui_SharpenAmount));

#ifndef DONTDODEPTH
    // Original Vanilla Logic
    if (doDepthOfField && cbDepthOfFieldEnabled)
    {
        result = DoDepthOfField(IN, result, depth); // [cite: 63]
    }

        // New Hybrid Logic
    else if (ui_DepthOfField)
    {
        result = applyHybridBokehDoF(IN, result, depth);
    }
#endif

        // PseudoHDR (Keep If, needed)
        if (ui_PseudoHDR > 0.5) result.xyz = DeCompressColourSpace(result.xyz);

        // HDR (Keep If)
        if (ui_UseHDR > 0.5)
        { 
            float4 hdr = applyHDR(IN, result); 
            result.rgb = hdr.rgb;
        }

        // PostFX (Keep If)
        if (ui_UsePostEffects) result = ShaderPass(IN, result);

        // Apply Brightness adjustments
        result.rgb *= ui_Brightness;

        // NIS fade
        if (doColourFade) result.xyz = lerp(result.xyz, cvVisualEffectFadeColour.xyz, cvVisualEffectFadeColour.w);

        return result;
}
#endif